import type { AssistantMessage, ImageContent, SystemMessage, ToolResultMessage, UserMessage, } from "@earendil-works/pi-ai"; import { getCurrentSystemMessage, getToolStateChanges, } from "@earendil-works/pi-ai/utils/transcript"; import type { AgentState, ConversationView, EntryRecord, LiveState, } from "@earendil-works/pi-durable"; import type { JsonValue } from "@berry/api"; import type { ShellExecution } from "@berry/api/shell.ts"; import type { CallStatus, ToolCall, ToolRenderer, ToolView } from "@berry/api/web.ts"; import { code } from "./code.tsx"; import { markdown, markdownView } from "./markdown.ts"; import { toolRenderer } from "./renderers.ts"; import { event } from "./event.tsx"; import { isImageResult, toolResult } from "./tool-result.ts"; import { type Handle, json, keyed, Scope } from "./ui.ts"; import { watch } from "./client.ts"; import { scroller, type Viewport } from "./scroller.tsx"; type Block = AssistantMessage["content"][number]; type ToolCallBlock = Extract; type NestedCall = { name: string; extension: string; args: JsonValue; status: "running" | "ok" | "error" | "cancelled"; startedAt?: number; durationMs?: number; output?: string; details?: JsonValue; value?: JsonValue; error?: string; }; const EMPTY: readonly never[] = []; const IDLE: LiveState = {}; const textOf = (content: readonly { type: string; text?: string }[]) => content .flatMap((block) => block.type === "text" && block.text ? [block.text] : []) .join("\n"); const userText = ({ content }: UserMessage) => typeof content === "string" ? content : textOf(content); /** Give an element its text, hiding it while there is none. */ function show(element: HTMLElement, text: string) { if (element.textContent === text) return; element.textContent = text; element.hidden = text === ""; } /** * How a card shows its body until the reader opens it: a `preview`, cut at a height; or `closed`, its header alone, * as a script's calls are, whose output tells what they did. */ type Rest = "preview" | "closed"; /** `250ms`, `12s`, `3m 07s`, `1h 02m`. */ function duration(ms: number): string { if (ms < 1000) return `${Math.floor(ms)}ms`; const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); if (minutes === 0) return `${seconds}s`; if (minutes < 60) { return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`; } return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}m`; } // one observer for every card, rather than one each; every card measures before any lays itself out again, so that // the page lays out once rather than once a card const measures = new Map (() => void) | undefined>(); const resizes = new ResizeObserver((entries) => { const layouts = entries.map(({ target }) => measures.get(target)?.()); for (const layout of layouts) layout?.(); }); type Card = Handle & { update(call: ToolCall): void; dispose(): void; }; function cardFrame( heading: Element[], content: HTMLElement, initial: Rest | "open" = "preview", /** Called whenever it shows its body, which may build it. */ reveal = () => {}, ) { const rest = initial === "open" ? "closed" : initial; let view: Rest | "open" = initial; let overflowing = false; const header = ( ) as HTMLElement; const blur = ( ) as HTMLElement; const more = ( ) as HTMLElement; const less = ( ) as HTMLElement; const preview = (
{content} {blur} {more}
) as HTMLElement; const element = (
{header} {preview} {less}
) as HTMLElement; function layout() { if (view !== "closed") reveal(); element.dataset.view = view; element.dataset.overflowing = String(overflowing); header.setAttribute( "aria-expanded", String(view === "open" || (view === "preview" && !overflowing)), ); more.hidden = view !== "preview" || !overflowing; less.hidden = view !== "open"; blur.hidden = more.hidden; content.inert = !more.hidden; } const measure = () => { const next = content.offsetHeight > preview.clientHeight + 1; if (next === overflowing) return; return () => { overflowing = next; layout(); }; }; layout(); for (const observed of [content, preview]) { measures.set(observed, measure); resizes.observe(observed); } return { element, expand() { view = "open"; layout(); }, dispose() { for (const observed of [content, preview]) { resizes.unobserve(observed); measures.delete(observed); } }, }; } function card( call: ToolCall, renderer: ToolRenderer | undefined, makeBody: () => ToolView = () => renderer?.body?.() ?? defaultBody(), initial: Rest | "open" = renderer?.initial ?? "preview", ): Card { let shown: ToolCall | null = null; let ticking: ReturnType | undefined; const summary = as HTMLElement; const time =